跳到主要内容

C 26-37 指针与数组

26 指针的本质分析

程序中的变量只是一段存储空间的别名,那么是不是必须通过这个别名才能使用这段存储空间?

下面的程序输出什么?为什么?

int i=5;
int *p = &i;
printf("%d,%p\n",i,p);
*p = 10;
printf("%d,%p\n",i,p);
  • 在指针声明时,*号表示所声明的变量为指针
  • 在指针使用时,*号表示取指针所指向的内存空间中的值
int i=0;
int j=0;
//指针声明
int *p=&i;
//取值
j = *p;

*号类似一把钥匙,通过这把钥匙可以打开内存,读取内存中的值。

  • 小贴士
int i = 0;
int j = 0;
int *p = &i;
j = *p;
/*变量p保存着变量i的内存地址,即:
p<-->&i
*p<-->i
*/

实例分析

  • 指针使用示例
#include <stdio.h>

int main()
{
int i = 0;
int* pI;
char* pC;
float* pF;

pI = &i;

*pI = 10;

printf("%p, %p, %d\n", pI, &i, i);
printf("%d, %d, %p\n", sizeof(int*), sizeof(pI), &pI);
printf("%d, %d, %p\n", sizeof(char*), sizeof(pC), &pC);
printf("%d, %d, %p\n", sizeof(float*), sizeof(pF), &pF);

return 0;
}

  • 指针是变量,因此可以声明指针参数
  • 当一个函数体内部需要改变实参的值,则需要使用指针参数
  • 函数调用时实参值将复制到形参
  • 指针适用于复杂数据类型作为参数的函数中

编程实验

  • 利用指针交换变量
#include <stdio.h>

int swap(int* a, int* b)
{
int c = *a;

*a = *b;

*b = c;
}

int main()
{
int aa = 1;
int bb = 2;

printf("aa = %d, bb = %d\n", aa, bb);

swap(&aa, &bb);

printf("aa = %d, bb = %d\n", aa, bb);

return 0;
}

  • const int *p //p可变,p指向的内容不可变
  • int const *p; //p可变,p指向的内容不可变
  • int * const p; //p不可变,p指向的内容可变
  • const int * const p; //p和p指向的内容都不可变

口诀:左数右指 当const出现在*号左边时指针指向的数据为常量,当const出现在*号右边时指针本身为常量。

实例分析

  • 常量与指针
#include <stdio.h>

int main()
{
int i = 0;

const int* p1 = &i;
int const* p2 = &i;
int* const p3 = &i;
const int* const p4 = &i;

*p1 = 1; // compile error
p1 = NULL; // ok

*p2 = 2; // compile error
p2 = NULL; // ok

*p3 = 3; // ok
p3 = NULL; // compile error

*p4 = 4; // compile error
p4 = NULL; // compile error

return 0;
}

小结

  • 指针是C语言中一种特别的变量
  • 指针所保存的值是内存的地址
  • 可以通过指针修改内存中任意地址内容

27 数组的本质分析

  • 数组是相同类型的变量的有序集合

数组的大小

  • 数组在一片连续的内存空间中存储元素
  • 数组的个数可以显示或隐式指定
int a[5]={1,2};
int b[]={1,2};

问题:

  1. a[2],a[3],a[4]的值是多少?
  2. b包含了多少个元素?

编程实验

  • 数组的初始化
#include <stdio.h>

int main()
{
int a[5] = {1, 2};
int b[] = {1, 2};

printf("a[2] = %d\n", a[2]);
printf("a[3] = %d\n", a[3]);
printf("a[4] = %d\n", a[4]);

printf("sizeof(a) = %d\n", sizeof(a));
printf("sizeof(b) = %d\n", sizeof(b));
printf("count for a: %d\n", sizeof(a)/sizeof(int));
printf("count for b: %d\n", sizeof(b)/sizeof(int));

return 0;
}

  • 数组名代表数组首元素的地址
  • 数组的地址需要用取地址符&才能得到
  • 数组首元素的地址与数组的地址值相同
  • 数组首元素的地址与数组的地址是两个不同的概念

编程实验

  • 数组名和数组地址
#include <stdio.h>

int main()
{
int a[5] = { 0 };

printf("a = %p\n", a);
printf("&a = %p\n", &a);
printf("&a[0] = %p\n", &a[0]);

return 0;
}
  • 数组名可以看作一个常量指针
  • 数组名“指向”的是内存中数组首元素的起始位置
  • 数组名不包含数组的长度信息
  • 在表达式中数组名只能作为右值使用
  • 只有在下列场合中数组名不能看作常量指针
    • 数组名作为sizeof操作符的参数
    • 数组名作为&运算符的参数

实例分析

  • 数组和指针并不相同
#include <stdio.h>

int main()
{
int a[5] = {0};
int b[2];
int* p = NULL;

p = a;

printf("a = %p\n", a);
printf("p = %p\n", p);
printf("&p = %p\n", &p);
printf("sizeof(a) = %d\n", sizeof(a));
printf("sizeof(p) = %d\n", sizeof(p));

printf("\n");

p = b;

printf("b = %p\n", b);
printf("p = %p\n", p);
printf("&p = %p\n", &p);
printf("sizeof(b) = %d\n", sizeof(b));
printf("sizeof(p) = %d\n", sizeof(p));

b = a;

return 0;
}

小结

  • 数组是一片连续的内存空间
  • 数组的地址和数组首元素的地址意义不同
  • 数组名在大多数情况下被当成常量指针处理
  • 数组名其实并不是指针,不能将其等同于指针

概念的混淆是BUG的根源之一!

28 指针和数组分析(上)

  • 数组是一段连续的内存空间
  • 数组的空间大小为sizeof(array_type)*array_size
  • 数组名可看作指向数组第一个元素的常量指针

问题:

  1. a+1的意义是什么?结果是什么?
  2. 指针运算的意义是什么?结果又是什么?

编程实验

  • a+1的结果是什么?
#include <stdio.h>

int main()
{
int a[5] = {0};
int* p = NULL;

printf("a = 0x%X\n", (unsigned int)(a));
printf("a + 1 = 0x%X\n", (unsigned int)(a + 1));

printf("p = 0x%X\n", (unsigned int)(p));
printf("p + 1 = 0x%X\n", (unsigned int)(p + 1));

return 0;
}

  • 指针是一种特殊的变量,与整数的运算规则为
  • p+n; <--> (unsigned int)p+n*sizeof(\*p);

结论:

当指针p指向一个同类型的数组的元素时:p+1将指向当前元素的下一个元素;p-1将指向当前元素的上一个元素

  • 指针之间只支持减法运算
  • 参与减法运算的指针类型必须相同

p1-p2; <--> (unsigned int)p1-(unsigned int)p2/sizeof(type);

注意:

  1. 只有当两个指针指向同一个数组的元素时,指针相减才会有意义,其意义为指针所指元素的下标差
  2. 当两个指针指向的元素不在同一个数组中时,结果未定义
  • 指针也可以进行关系运算<,<=,>,>=
  • 指针关系运算的前提是同时指定同一个数组中的元素
  • 任意两个指针之间的比较运算(==,!=)无限制
  • 参与比较运算的指针类型必须相同

实例分析

  • 指针运算初探
#include <stdio.h>

int main()
{
char s1[] = {'H', 'e', 'l', 'l', 'o'};
int i = 0;
char s2[] = {'W', 'o', 'r', 'l', 'd'};
char* p0 = s1;
char* p1 = &s1[3];
char* p2 = s2;
int* p = &i;

printf("%d\n", p0 - p1);
printf("%d\n", p0 + p2);
printf("%d\n", p0 - p2);
printf("%d\n", p0 - p);
printf("%d\n", p0 * p2);
printf("%d\n", p0 / p2);

return 0;
}

  • 指针运算的应用
#include <stdio.h>

#define DIM(a) (sizeof(a) / sizeof(*a))

int main()
{
char s[] = {'H', 'e', 'l', 'l', 'o'};
char* pBegin = s;
char* pEnd = s + DIM(s); // Key point
char* p = NULL;

printf("pBegin = %p\n", pBegin);
printf("pEnd = %p\n", pEnd);

printf("Size: %d\n", pEnd - pBegin);

for(p=pBegin; p<pEnd; p++)
{
printf("%c", *p);
}

printf("\n");

return 0;
}

小结

  • 数组声明时编译器自动分配一片连续的内存空间
  • 指针声明时只分配了用于容纳地址值的4字节空间
  • 指针和数组可以进行运算,其结果为指针
  • 指针之间只支持减法运算,其结果为数组元素下标差
  • 指针之间支持比较运算,其类型必须相同

29 指针和数组分析(下)

数组名可以当作常量指针使用,那么指针是否也可以当作数组名来使用呢?

  • 以下标的形式访问数组中的元素
int main(){
int a[5]={0};
a[1]=3;
a[2]=5;
return 0;
}
  • 以指针的形式访问数组中的元素
int mian(){
int a[5]={0};
*(a+1)=3;
*(a+2)=5;
return 0;
}
  • 指针以固定增量在数组中移动时,效率高于下标形式

  • 指针增量为1且硬件具有硬件增量模型时,效率更高

  • 下标形式与指针形式的转换

    a[n] <--> *(a+n) <--> *(n+a) <--> n[a]

注意:现代编译器的生成代码优化率已大大提高,在固定增量时,下标形式的效率已经和指针形式相当;但从可读性和代码维护的角度来看,下标形式更优。

实例分析

  • 数组的访问方式
#include <stdio.h>

int main()
{
int a[5] = {0};
int* p = a;
int i = 0;

for(i=0; i<5; i++)
{
p[i] = i + 1;
}

for(i=0; i<5; i++)
{
printf("a[%d] = %d\n", i, *(a + i));
}

printf("\n");

for(i=0; i<5; i++)
{
i[a] = i + 10;
}

for(i=0; i<5; i++)
{
printf("p[%d] = %d\n", i, p[i]);
}

return 0;
}

  • 数组和指针不同
//ext.c
int a[] = {1, 2, 3, 4, 5};

#include <stdio.h>

int main()
{
extern int* a;

printf("&a = %p\n", &a);
printf("a = %p\n", a);
printf("*a = %d\n", *a);


return 0;
}
//会发生段错误,数组名不是指针

#a和&a的区别

  • a为数组首元素的地址

  • &a为整个数组的地址

  • a和&a的区别在于指针运算

    a+1->(unsigned int)a+sizeof(*a)

    &a+1->(unsigned int)(&a)+sizeof(*&a)->(unsigned int)(&a)+sizeof(a)

实例分析

  • 指针运算经典问题
#include <stdio.h>

int main()
{
int a[5] = {1, 2, 3, 4, 5};
int* p1 = (int*)(&a + 1);
int* p2 = (int*)((int)a + 1);
int* p3 = (int*)(a + 1);

printf("%d, %d, %d\n", p1[-1], p2[0], p3[1]);

return 0;
}
// A. 数组下标不能是负数,程序无法运行
// B. p1[-1]将输出随机数,p2[0]输出2, p3[1]输出3
// C. p1[-1]将输出乱码, p2[0]和p3[1]输出2

  • 数组作为参数时,编译器将其编译成对应的指针

    void f(int a[]); <--> void f(int *a);

    void f(int a[5]); <--> void f(int *a);

结论:一般情况下,当定义的函数中有数组参数时,需要定义另一个参数来标示数组的大小

实例分析

  • 虚幻的数组参数
#include <stdio.h>

void func1(char a[5])
{
printf("In func1: sizeof(a) = %d\n", sizeof(a));

*a = 'a';

a = NULL;
}

void func2(char b[])
{
printf("In func2: sizeof(b) = %d\n", sizeof(b));

*b = 'b';

b = NULL;
}

int main()
{
char array[10] = {0};

func1(array);

printf("array[0] = %c\n", array[0]);

func2(array);

printf("array[0] = %c\n", array[0]);

return 0;
}

#小结

  • 数组名和指针仅使用方式相同
    • 数组名的本质不是指针
    • 指针的本质不是数组
  • 数组名并不是数组的地址,而是数组首元素的地址
  • 函数的数组参数退化为指针

30 C语言中的字符串

  • 字符串是有序字符的集合
  • 字符串是程序中的基本元素之一
  • C语言中没有字符串的概念
    • C语言通过特殊的字符串数组模拟字符串
    • C语言中的字符串是以'\0'结尾的字符数组
  • 在C语言中,双引号引用的单个或多个字符是一种特殊的字面量
    • 存储于程序的全局只读存储区
    • 本质为字符数组,编译器自动在结尾加上'\0'字符

下面哪些是字符串的定义?

char ca[]={'H','e','l','l','o'};
char sa[]={'W','o','r','l','d','0'};
char ss[]="Hello world!";
char *str = "Hello world!";

实例分析

  • 字符数组与字符串
#include <stdio.h>

int main()
{
char ca[] = {'H', 'e', 'l', 'l', 'o'};
char sa[] = {'W', 'o', 'r', 'l', 'd', '\0'};
char ss[] = "Hello world!";
char* str = "Hello world!";

printf("%s\n", ca);
printf("%s\n", sa);
printf("%s\n", ss);
printf("%s\n", str);

return 0;
}

  • 你知道吗?
    • 字符串字面量的本质是一个数组
    • 字符串字面量可以看作常量指针
    • 字符串字面量中的字符不可改变
    • 字符串字面量至少包含一个字符
    • "Hello World!"是一个无名的字符数组

下面的表达式正确吗?

char b = "abc"[0];
char c= *("123"+1);
char t = *''';

实例分析

  • 字符串字面量的本质
#include <stdio.h>

int main()
{
char b = "abc"[0];
char c = *("123" + 1);
char t = *"";

printf("%c\n", b);
printf("%c\n", c);
printf("%d\n", t);

printf("%s\n", "Hello");
printf("%p\n", "World");

return 0;
}

  • 字符串的长度就是字符串所包含字符的个数
  • 字符串长度指的是第一个'\0'字符前出现的字符个数
  • 通过'\0'结束符来确定字符串的长度
  • 函数strlen用于返回字符串的长度
char s[]="Hello";
printf("%d\n",strlen(s));
printf("%d\n",strlen("123"));

编程实验

  • strlen的使用
#include <stdio.h>
#include <string.h>

int main()
{
char s[] = "Hello\0world";
int i = 0;

for(i=0; i<sizeof(s)/sizeof(char); i++)
{
printf("%c\n", s[i]);
}

printf("%s\n", s);

printf( "%d\n", strlen(s) );
printf( "%d\n", strlen("123") );


return 0;
}

小结

  • C语言中通过字符数组模拟字符串
  • C语言中的字符串使用'\0'作为结束符
  • 字符字面量的本质为字符数组
  • 字符串相关函数都依赖于结束符'\0'

31 字符串典型问题分析

  • 下面的程序输出什么为什么?
#include <stdio.h>

int main()
{
char buf[10] = {0};
char src[] = "hello %s";

snprintf(buf, sizeof(buf), src);

printf("buf = %s\n", buf);

return 0;
}

  • snprintf函数本身是可变参数函数,原型如下:
int snprintf(char *buffer,int buf_size,const char *format,...)

当函数只有3个参数时,如果第三个参数没有包含格式化信息,调用函数没有问题;相反,如果第三个参数包含了格式化信息,但缺少后续对应参数,则程序行为不确定。 格式化信息必须与变参个数相匹配。

  • 下面的程序输出什么为什么?
#include <stdio.h>
#include <string.h>

int main()
{
#define STR "Hello, \0D.T.Software\0"

char* src = STR;
char buf[255] = {0};

snprintf(buf, sizeof(buf), src);

printf("strlen(STR) = %d\n", strlen(STR));
printf("sizeof(STR) = %d\n", sizeof(STR));

printf("strlen(src) = %d\n", strlen(src));
printf("sizeof(src) = %d\n", sizeof(src));

printf("strlen(buf) = %d\n", strlen(buf));
printf("sizeof(buf) = %d\n", sizeof(buf));

printf("src = %s\n", src);
printf("buf = %s\n", buf);

return 0;
}

  • 字符串相关的函数均以第一个出现的'\0'作为结束符
  • 编译器总是会在字符串字面量的末尾添加'\0'
  • 字符串字面量的本质为数组

  • 下面的程序输出什么为什么?
#include <stdio.h>
#include <string.h>

int main()
{
#define S1 "Hello World!"
#define S2 "Hello World!"

if( S1 == S2 )
{
printf("Equal\n");
}
else
{
printf("Non Equal\n");
}

if( strcmp(S1, S2) == 0 )
{
printf("Equal\n");
}
else
{
printf("Non Equal\n");
}

return 0;
}

  • 字符串之间的相等比较需要用strcmp完成
  • 不可直接用==进行字符串直接的比较
  • 完全相同的字符串字面量的==比较结果为false

一些现在编译器能够将相同的字符串字面量映射到同一个无名字符数组,因此==比较结果为true。

不编写依赖特殊编译器的代码!

  • 字符串循环右移
void right_shift_r(const char *src,char *result,unsigned int n);

函数功能:将输入字符串src循环右移n位,result为输出结果。

要求:以效率最高的方式实现。

示例:

"abcde"--2-->"deabc"

"abcde"--8-->"cdeab"
#include <stdio.h>
#include <string.h>

void right_shift_r(const char* src, char* result, unsigned int n)
{
const unsigned int LEN = strlen(src);
int i = 0;

for(i=0; i < LEN; i++)
{
result[(n + i) % LEN] = src[i];
}

result[LEN] = '\0';
}

int main()
{
char result[255] = {0};

right_shift_r("abcde", result, 2);

printf("%s\n", result);

right_shift_r("abcde", result, 5);

printf("%s\n", result);

right_shift_r("abcde", result, 8);

printf("%s\n", result);

return 0;
}

32 数组指针和指针数组分析

  • 下面这些声明合法吗?
int array[5];
int matrix[3][3];
int *pa = array;
int *pm = matrix;

问题:

array代表数组首元素的地址,那么matrix代表什么?

array和&array的地址值相同,但是意义不同,那么它们所代表的类型相同吗?

  • C语言中的数组有自己特定的类型
  • 数组的类型由元素类型和数组大小共同决定

例:int array[5]的类型为int[5]

工作中的对话......

A:这里定义的数组什么类型的?

B:int型...... ERROR!

  • C语言中通过typedef为数组类型重命名
typedef type(name)[size];
  • 数组类型:
typedef int(AINT5)[5];
typedef float(AFLOAT10)[10];
  • 数组定义:
AINT5 iArray;
AFLOAT10 fArray;
  • 数组指针用于指向一个数组
  • 数组名是数组首元素的起始地址,但并不是数组的起始地址
  • 通过将取地址符&作用于数组名可以得到数组的起始地址
  • 可通过数组类型定义数组指针:ArrayType* pointer;
  • 也可以直接定义:type(*pointer)[n];

pointer为数组指针变量名,type为指向的数组的元素类型,n为指向的数组的大小。

实例分析

  • 数组类型和数组指针
#include <stdio.h>

typedef int(AINT5)[5];
typedef float(AFLOAT10)[10];
typedef char(ACHAR9)[9];

int main()
{
AINT5 a1;
float fArray[10];
AFLOAT10* pf = &fArray;
ACHAR9 cArray;

char(*pc)[9] = &cArray;
char(*pcw)[4] = cArray; //error

int i = 0;

printf("%d, %d\n", sizeof(AINT5), sizeof(a1));//20,20

for(i=0; i<10; i++)
{
(*pf)[i] = i;
}

for(i=0; i<10; i++)
{
printf("%f\n", fArray[i]);
}

printf("%p, %p, %p\n", &cArray, pc+1, pcw+1);

return 0;
}
  • 指针数组是一个普通的数组
  • 指针数组中的每个元素为一个指针
  • 指针数组的定义:type* pArray[n];

type*为数组中每个元素的类型pArray为数组名,n为数组大小。

实例分析

  • 指针数组的应用
#include <stdio.h>
#include <string.h>

#define DIM(a) (sizeof(a)/sizeof(*a))

int lookup_keyword(const char* key, const char* table[], const int size)
{
int ret = -1;

int i = 0;

for(i=0; i<size; i++)
{
if( strcmp(key, table[i]) == 0 )
{
ret = i;
break;
}
}

return ret;
}

int main()
{
const char* keyword[] = {
"do",
"for",
"if",
"register",
"return",
"switch",
"while",
"case",
"static"
};

printf("%d\n", lookup_keyword("return", keyword, DIM(keyword)));
printf("%d\n", lookup_keyword("main", keyword, DIM(keyword)));

return 0;
}

小结

  • 数组的类型由元素类型和数组大小共同决定
  • 数组指针是一个指针,指向对应类型的数组
  • 指针数组是一个数组,其中每个元素都为指针
  • 数组指针遵循指针运算法则
  • 指针数组拥有C语言数组的各种特性

33 main函数与命令行参数

  • C语言中mian函数称之为主函数
  • 一个C程序是从main函数开始执行的
//下面的main函数定义正确吗?
main(){

}

void main(){

}

int main(){

}

int main(){
return 0;
}

编程实验

  • main函数的原型究竟是什么?

int main()
{
return 0;
}

  • main函数是操作系统调用的函数
  • 操作系统总是将main函数作为应用程序的开始
  • 操作系统将main函数的返回值作为程序的退出状态

思考:为什么C编译器支持那么多不同的main函数原型?

编程实验

  • main函数返回值
#include <stdio.h>

int main()
{
printf("I'm A!\n");
return 0;
}
#include <stdio.h>

int main()
{
printf("I'm B!\n");
return 99;
}
  • 程序执行时可以向main函数传递参数
int main();
int main(int argc);
int main(int argc,char *argv[]);
int main(int argc,char *argv[],char *env[])
/*
argc命令行参数个数
argv命令行参数数组
env环境变量数组
*/
  • gcc编译器的常见用法
#argc -> 4
#argv[0] -> gcc
#argv[1] -> a.c
#argv[2] -> b.c
#argv[3] -> c.c
gcc a.c b.c c.c

实例分析

  • main函数的参数
#include <stdio.h>

int main(int argc, char* argv[], char* env[])
{
int i = 0;

printf("============== Begin argv ==============\n");

for(i=0; i<argc; i++)
{
printf("%s\n", argv[i]);
}

printf("============== End argv ==============\n");

printf("\n");
printf("\n");
printf("\n");

printf("============== Begin env ==============\n");

for(i=0; env[i]!=NULL; i++)
{
printf("%s\n", env[i]);
}

printf("============== End env ==============\n");

return 0;
}


  • 面试中的小问题 main函数一定是程序执行的第一个函数吗?

实例分析

  • gcc的属性关键字
#include <stdio.h>

#ifndef __GNUC__
#define __attribute__(x)
#endif

__attribute__((constructor))
void before_main()
{
printf("%s\n",__FUNCTION__);
}

__attribute__((destructor))
void after_main()
{
printf("%s\n",__FUNCTION__);
}

int main()
{
printf("%s\n",__FUNCTION__);

return 0;
}

小结

  • 一个C程序是从main函数开始执行的
  • main函数是操作系统调用的函数
  • main函数有参数和返回值
  • 现代编译器支持在main函数前调用其它函数

34 多维数组和多维指针

  • 指针的本质是变量
  • 指针会占用一定的内存空间
  • 可以定义指针的指针来保存指针变量的地址值
int mian() {
int i=0;
int *p = nullptr;
int **pp = nullptr;
pp = &p;
*pp = &i;
retutn 0;
}

为什么需要指向指针的指针?

  • 指针在本质上也是变量
  • 对于指针也同样存在传值调用传址调用

实例分析:重置动态空间大小

#include <stdio.h>
#include <malloc.h>

int reset(char**p, int size, int new_size) {
int ret = 1;
int i = 0;
int len = 0;
char* pt = NULL;
char* tmp = NULL;
char* pp = *p;

if( (p != NULL) && (new_size > 0) ) {
pt = (char*)malloc(new_size);
tmp = pt;
len = (size < new_size) ? size : new_size;
for(i=0; i<len; i++) {
*tmp++ = *pp++;
}
free(*p);
*p = pt;
} else {
ret = 0;
}
return ret;
}

int main() {
char* p = (char*)malloc(5);
printf("%p\n", p);
if( reset(&p, 5, 3) ) {
printf("%p\n", p);
}
free(p);
return 0;
}

二维数组与二级指针

  • 二维数组在内存以一维的方式排布
  • 二维数组中的第一维是一维数组
  • 二维数组中的第二维才是具体的值
  • 二维数组的数组名可看作常量指针

实例分析:遍历二维数组

#include <stdio.h>
#include <malloc.h>

void printArray(int a[], int size) {
int i = 0;
printf("printArray: %d\n", sizeof(a));
for(i=0; i<size; i++) {
printf("%d\n", a[i]);
}
}

int main() {
int a[4][3] = {
{0, 1, 2},
{3, 4, 5},
{6, 7, 8},
{9, 10, 11}
};
int* p = &a[0][0];

for(int i=0; i<4; i++) {
for(int j=0; j<3; j++) {
printf("%d, ", *(*(a+i) + j)); // 等价于 printf("%d, ", a[i][j]);
}
printf("\n");
}
printf("\n");
printArray(p, 9);
return 0;
}
  • 一维数组名代表数组首元素的地址 int a[5]: a的类型为int*
  • 二维数组名同样代表数组首元素的地址 int m[2][5]: m的类型为int(*)[5];

结论:

  1. 二维数组名可以看做是指向数组的常量指针
  2. 二维数组可以看作是一维数组
  3. 二维数组中的每个元素都是同类型的一维数组

实例分析:如何动态申请二维数组

#include <stdio.h>
#include <malloc.h>

int** malloc2d(int row, int col) {
int** ret = NULL;
if( (row > 0) && (col > 0) ) {
ret = (int**)malloc(row * sizeof(int*));
int *p = (int*)malloc(row * col * sizeof(int));
if( (ret != NULL) && (p != NULL) ) {
for(int i=0; i<row; i++) {
ret[i] = p + i * col;
}
} else {
free(ret);
free(p);
ret = NULL;
}
}
return ret;
}

void free2d(int** p) {
if( *p != NULL ) {
free(*p);
}
free(p);
}

int main() {
int** a = malloc2d(2, 3);
a[0][0] = 4; a[0][1] = 7; a[0][2] = 9;
a[1][0] = 11; a[1][1] = 6; a[1][2] = 3;
for(int i=0; i<2; i++) {
for(int j=0; j<3; j++) {
printf("%d, ", a[i][j]);
}
printf("\n");
}
free2d(a);
return 0;
}

小结

  • C语言中只支持一维数组
  • C语言中的数组大小必须在编译器就作为常数确定
  • C语言中的数组元素可以是任意类型的数据
  • C语言中的数组的元素可以是另一个数组

35 数组参数和指针参数分析

为什么C语言中的数组参数会退化为指针?

  • C语言中只会以值拷贝的方式传递参数
  • 当向函数传递数组时:
    • 将整个数组拷贝一份传入函数 ERROR
    • 将数组名看作常量指针传入数组元素首地址

C语音以高效作为最初设计目标:

  1. 参数传递的时候如果拷贝整个数组执行效率将大大下降。
  2. 参数位于栈上,太大的数组拷贝将导致栈溢出。
  • 二维数组参数同样存在退化的问题
    • 二维数组可以看作是一维数组
    • 二维数组中的每一个元素都是一维数组
  • 二维数组参数中第一维的参数可以省略
    • void f(int a[5])<-->void f(int a[])<-->void f(int *a)
    • void g(int a\[3][3])<-->void g(int a\[][3])<-->void g(int (*a)[3])
数组参数等效的指针参数
一维数组:float a[5]指针:float *a
指针数组 int* a[5]指针的指针:int* *a
二维数组:char a[3][4]数组的指针:char(*a)[4]
- C语言中无法向一个函数传递任意的多维数组
- 必须提供除第一维之外的所有长度
- 第一维之外的维度信息用于完成指针运算
- N维数组的本质是一维数组,元素是N-1维的数组
- 对于多维数组的函数参数只有第一维是可变的

实例分析

  • 传递与访问二维数组
#include <stdio.h>

void access(int a[][3], int row)
{
int col = sizeof(*a) / sizeof(int);
int i = 0;
int j = 0;

printf("sizeof(a) = %d\n", sizeof(a));
printf("sizeof(*a) = %d\n", sizeof(*a));

for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d\n", a[i][j]);
}
}

printf("\n");
}

void access_ex(int b[][2][3], int n)
{
int i = 0;
int j = 0;
int k = 0;

printf("sizeof(b) = %d\n", sizeof(b));
printf("sizeof(*b) = %d\n", sizeof(*b));

for(i=0; i<n; i++)
{
for(j=0; j<2; j++)
{
for(k=0; k<3; k++)
{
printf("%d\n", b[i][j][k]);
}
}
}

printf("\n");
}

int main()
{
int a[3][3] = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}};
int aa[2][2] = {0};
int b[1][2][3] = {0};

access(a, 3);
access(aa, 2);
access_ex(b, 1);
access_ex(aa, 2);

return 0;
}


小结

  • C语言中只会以值拷贝的方式传递参数
  • C语言中的数组参数必然退化为指针
  • 多维数组参数必须提供除第一维之外的所有维长度
  • 对于多维数组的函数参数只有第一维是可变的

36 函数与指针分析

函数类型

  • C语言中的函数有自己特定的类型
  • 函数的类型由返回值,类型参数和参数个数共同决定 int add(int i,int j)的类型为int(int,int)
  • C语言中通过typedef为函数类型重命名 typedef type name(parameter list)
  • 例: typedef int f(int,int); typedef void p(int);

函数指针

  • 函数指针用于指向一个函数
  • 函数名是执行函数体的入口地址
  • 可通过函数类型定义函数指针:FuncType *pointer
  • 也可以直接定义:type (*pointer)(parameter list)
    • pointer为函数指针变量名
    • type为所指函数的返回值类型
    • parameter list为所指函数的参数类型列表

面试小问题

如何使用C语言直接跳转到某个固定的地址开始执行?

实例分析

  • 函数指针的使用
#include <stdio.h>

typedef int(FUNC)(int);

int test(int i)
{
return i * i;
}

void f()
{
printf("Call f()...\n");
}

int main()
{
FUNC* pt = test;
void(*pf)() = &f;

printf("pf = %p\n", pf);
printf("f = %p\n", f);
printf("&f = %p\n", &f);

pf();

(*pf)();

printf("Function pointer call: %d\n", pt(2));

return 0;
}


回调函数

  • 回调函数是利用函数指针实现的一种调用机制
  • 回调机制原理
    • 调用者不知道具体事件发生时需要调用的具体参数
    • 被调函数不知道何时被调用,只知道需要完成的任务
    • 当具体事件发生时,调用者通过函数指针调用具体函数
  • 回调机制中的调用者和被调函数互不依赖

实例分析

  • 回调函数使用示例
#include <stdio.h>

typedef int(*Weapon)(int);

void fight(Weapon wp, int arg)
{
int result = 0;

printf("Fight boss!\n");

result = wp(arg);

printf("Boss loss: %d\n", result);
}

int knife(int n)
{
int ret = 0;
int i = 0;

for(i=0; i<n; i++)
{
printf("Knife attack: %d\n", 1);
ret++;
}

return ret;
}

int sword(int n)
{
int ret = 0;
int i = 0;

for(i=0; i<n; i++)
{
printf("Sword attack: %d\n", 5);
ret += 5;
}

return ret;
}

int gun(int n)
{
int ret = 0;
int i = 0;

for(i=0; i<n; i++)
{
printf("Gun attack: %d\n", 10);
ret += 10;
}

return ret;
}

int main()
{
fight(knife, 3);
fight(sword, 4);
fight(gun, 5);

return 0;
}


小结

  • C语言中的函数都有特定的类型
  • 可以使用函数类型定义函数指针
  • 函数指针是实现回调机制的关键技术
  • 通过函数指针可以在C程序中实现固定地址跳转

37 指针阅读技巧分析

笔试中的问题

下面的标识符代表什么含义?

int (*p1) (int*,int(*f)(int*));
int (*p2[5])(int*);
int (*(*p3)[5])(int*);
int* (*(*p4)(int*))(int*);
int* (*(*p4)(int*))(int*);
int (*(*p5)(int*))[5];

右左法则

  • 从最里面的圆括号中未定义的标识符看起
  • 首先往右看,再往左看
  • 遇到圆括号或方括号时可以确定部分类型,并调转方向
  • 重复2,3步骤,直到阅读结束

编程实验

  • 复杂指针的阅读
#include <stdio.h>

int main()
{
int (*p1)(int*, int (*f)(int*));

int (*p2[5])(int*);

int (*(*p3)[5])(int*);

int*(*(*p4)(int*))(int*);

int (*(*p5)(int*))[5];

return 0;
}


小结

  • 由左法则总结于编译器对指针变量的解析过程
  • 指针阅读练习的意义在于理解指针的组合定义
  • 可通过typedef简化对复杂指针的定义